Skip to content

(develop) Add a UI for fix file customization - #5294

Merged
TravisElless-NOAA merged 14 commits into
NOAA-EMC:developfrom
AlexanderHrabski-NOAA:feature/fix_overlay
Oct 9, 2026
Merged

TravisElless-NOAA merged 14 commits into
NOAA-EMC:developfrom
AlexanderHrabski-NOAA:feature/fix_overlay

Conversation

@AlexanderHrabski-NOAA

Copy link
Copy Markdown
Contributor

Description

Forecasting system development can involve testing changes to fix files. This PR adds support for fix file customization as an experimental parameter, eliminating the need for manually re-linking global workflow's fix file installation global-workflow/fix. Because these fix file edits are a parameter of the experiment, the UI takes the form of a new, optional section of the experiment setup yaml (at the level of base:) that reads like a diff from the global fix file installation, with a basic syntax of dst: src:

   base:
      ...

   {% set fix_008 = '/scratch4/NCEPDEV/marine/First.Last/fix_008' %}

   fix_files:
     replace:                                # each of these is already in the fix tree
       wave:                      /scratch4/NCEPDEV/marine/First.Last/testfix/wave     # directory for directory
       mom6/025/ocean_hgrid.nc:   {{ fix_008 }}/MOM/ocean_hgrid.nc                     # file for file
       orog/C384:                 {{ fix_008 }}/OROG/C384/C384.mx008_oro*              # each matched file is already in orog/C384
     add:                                    # none of these is in the fix tree yet
       cpl/aC384o008:             {{ fix_008 }}/CPL/aC384o008                          # new directory
       mom6/008/regional.mom6.nc: {{ fix_008 }}/MOM/regional.mom6.nc                   # new file
       mom6/008:                                                                          # new files into a (here also new) directory
         - {{ fix_008 }}/MOM/*zgrid*.nc
         - {{ fix_008 }}/MOM/MOM_*

dst is relative to the root fix/ directory, and src is always absolute. Note that using jinja is an option, but it is not necessary.

This UI is parsed by a new set of functions in dev/workflow/fix_overlay.py, with error handling to enforce the UI's spec. If fix_files: is included, the modified fix file tree is generated by a utility in fix_overlay.py into EXPDIR/$pslot/fix so it exists alongside the rest of the experiment's configuration, and the workflow is pointed at it via {{ FIXglobal }}. The tree is linked lazily, so its generation time scales with the pieces of the tree the user touches, rather than the size of the entire global fix file tree.

Unit tests are included for the new utilities. Because there are plenty of ways to break the UI, there a many error paths to test, leading to a relatively large number of new pytests. Despite their number, they are very fast to run.

There is no change in default behavior. This PR was co-authored by Claude Opus 5 and verified by myself.

A few loose ends:

  • A few paths in GDASApp use {{ HOMEglobal }}/fix directly, bypassing the override, so a follow-up PR will be needed there for the submodule to support this feature @guillaumevernieres. Note that several uses of {{ HOMEglobal }}/fix in this repo were replaced as part of this PR.

  • {{ FIXglobal }} is not available at the experiment-setup YAML's render time, so other, similar options like SOCA_INPUT_FIX_DIR must use ${FIXglobal} in their paths to use the override. Note that the GDAS fix files can be customized via this interface, so it may be worth revisiting these options if the new interface completely covers their use cases.

    Resolves Support FIX file customization #5202

Type of change

  • Bug fix (fixes something broken)
  • New feature (adds functionality)
  • Maintenance (code refactor, clean-up, new CI test, etc.)

Change characteristics

  • Is this change expected to change outputs (e.g. value changes to existing outputs, new files stored in COM, files removed from COM, filename changes, additions/subtractions to archives)? NO
  • Is this a breaking change (a change in existing functionality)? NO
  • Does this change require a documentation update? YES
  • Does this change require an update to any of the following submodules? YES (if GDASApp is to be supported, but this PR is functional on its own)
    • EMC verif-global
    • GDAS
    • GFS-utils
    • GSI
    • GSI-monitor
    • GSI-utils
    • UFS-utils
    • UFS-weather-model
    • wxflow

How has this been tested?

  • The new unit tests pass, verifying the UI and integration into setup_expt.py are correct.
  • I have ported an experimental 8km ocean/ice workflow based on GFS to this system and confirmed the workflow reproduces its correct behavior on Ursa. The workflow requires fix file customization.

Remaining:

  • GitLab CI testing

Checklist

  • Any dependent changes have been merged and published
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have documented my code, including function, input, and output descriptions
  • My changes generate no new warnings
  • New and existing tests pass with my changes
  • This change is covered by an existing CI test or a new one has been added
  • Any new scripts have been added to the .github/CODEOWNERS file with owners
  • I have made corresponding changes to the system documentation if necessary

… yaml

Add a top-level `fix:` section to the experiment yaml that overlays
user-supplied files on the installed fix tree. Entries are a flat
`dst: src` map with `dst` relative to the fix root; a plain source
replaces (file or directory), a glob source merges its matches into the
destination directory.

`setup_expt.py` builds the overlay in `${EXPDIR}/fix` with lazy
expansion: only directories on the path to an overlay entry become real
directories, everything else is a single symlink back to
`${HOMEglobal}/fix`. A manifest is written to `.fix_overlay.yaml`.
Missing sources, empty globs, escaping destinations and conflicting
entries are hard errors.

`config.base` now renders `FIXglobal` from the yaml, defaulting to
`${HOMEglobal}/fix`, so every `${FIXglobal}/...` reference follows the
overlay. The in-repo bypasses (`FIXgsi` and the three `config.aero`
chem paths) are switched from `${HOMEglobal}/fix` to `${FIXglobal}`.

Refs NOAA-EMC#5202
A glob merge followed by a single-file entry on the same leaf is the
natural way to write "all of these, except this one from elsewhere", so
duplicate leaves are now last-wins instead of an error. Each override is
logged and recorded under `overrides:` in the manifest. Entries nested
inside a wholesale-replaced directory remain an error.
…erge

Rename the experiment-yaml section from `fix:` to `fix_files:` and split
it into three sub-sections so the user states what they expect of the
fix tree, and setup_expt.py checks it:

  replace:  path must already exist (and be the same kind as the source)
  add:      path must not exist yet
  merge:    path must be an existing directory; every file in the source
            directory, or every glob match, is linked in beside the
            existing ones

A misspelled `replace` target or an `add` that would clobber a system
file is now an error instead of a silent no-op or overwrite. Globs are
only meaningful under `merge`, where they filter the source directory.
`replace`/`add` entries take precedence over `merge` matches regardless
of the order they are written in.

setup_expt.py ends with a summary block listing each entry with its
mode, destination, file count and `<-` source.
`fix_files:` now has only `replace:` and `add:`. A glob source is allowed
under either when the destination is a directory: each match is linked
into it under its own name and is checked individually against the rule
of its sub-section (must already exist and match kind under `replace`,
must not exist under `add`). A destination may take a list of sources
so several globs can target one directory under the same sub-section.

This removes the one place the interface let a misspelled source file
land silently as an extra file, and with it the merge/replace precedence
rules and the override records in the manifest: replace and add cannot
claim the same path, so any duplicate is simply an error.

The parent-is-a-file check moves before any directory is created, so a
failed setup never leaves a partial overlay behind. The `add` error for
a plain file onto an existing directory now suggests naming the full
path of the new file.
A plain file inside a list was treated like a single plain source, so
`replace: {orog/C384: [<glob>, C384_mosaic.nc]}` failed as a kind
mismatch (directory dst, file src). A list now consistently means
"these go into that directory": every item, glob or plain, is linked
under its own name at <dst>/<basename> and checked individually.
A `fix_files: add:` section in the experiment yaml renders FIXglobal
into config.base pointing at the overlay, links the requested files,
and leaves untouched components as single links. Only `add:` is
exercised because link_workflow.sh on the CI runner leaves dangling
component links, so nothing "exists" for `replace:`.
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Bash code analysis

✅ shellcheck and shfmt found no issues.

View the analysis run

@JessicaMeixner-NOAA

Copy link
Copy Markdown
Contributor

@AlexanderHrabski-NOAA I'll again voice my concern that this is a lot of code and extras to maintain when it seems like a two line command:

unlink fix_dir
link experimental_fix fix_dir

seems sufficient. It's really nice and thoughtful work, but I personally just don't think we need this to be this fancy.

@AlexanderHrabski-NOAA

Copy link
Copy Markdown
Contributor Author

@JessicaMeixner-NOAA I tried pretty hard to keep the diff in the existing workflow code small -- 15 lines of code excluding comments, the majority of which are either (a) setting a jinja default or (b) replacing instances of a hardcoded ${HOMEglobal}/fix with ${FIXglobal}, which is a variable that already exists for that purpose. The new code hooks in via setup_expt.py, is almost entirely skipped if the fix_files: section is absent, and is otherwise a self-contained module.

The RTOFS team's use case is a fair bit more than a 2-liner to set up, with a sizable and evolving set of changes to the default fix file tree. I'm counting over 50 additional files as of my last test, mostly to support the 8km ocean/ice resolution. We'd really benefit from something like this for the reasons I mentioned in #5202, especially when it comes to "configuration as code" for reproducibility among ourselves! It's very easy to forget how these fix file trees have evolved over time via manual linking as the diff from the default tree grows. By tracking the diff via the experiment's yaml and customizing the tree in the experiment's directory, these modifications are recorded and are isolated to a single experiment, all without having to maintain a full mirror of the fix file tree with various hand-edits (experimental_fix in your example).

@emcbot emcbot added CI-Ursa-Ready **CM use only** PR is ready for CI testing on Ursa CI-Ursa-Building **Bot use only** CI testing is cloning/building on Ursa and removed CI-Ursa-Ready **CM use only** PR is ready for CI testing on Ursa labels Sep 24, 2026
@DavidHuber-NOAA DavidHuber-NOAA added CI-Ursa-Passed **Bot use only** CI testing on Ursa for this PR has completed successfully and removed CI-Ursa-Building **Bot use only** CI testing is cloning/building on Ursa labels Sep 24, 2026
Comment thread dev/workflow/fix_overlay.py

@DavidHuber-NOAA DavidHuber-NOAA left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the technical aspects of this PR are well developed, tested, and documented, so approve on that basis.

I will add that this adds some level of complexity, but for the user the change is minimal, so there seems to be some level of trade off. The design itself, however, is clean and may be expandable to other areas of the workflow. Some functions (e.g. _is_glob, _kind for sure (the latter being expandable to soft and hard links), perhaps others) may be served better in a higher level Python script or class (perhaps ecflow), but for now seem fine where they are.

@emcbot emcbot added CI-Gaeac6-Ready **CM use only** PR is ready for CI testing on Gaea C6 CI-Gaeac6-Building **Bot use only** CI testing is cloning/building on Gaea C6 and removed CI-Gaeac6-Ready **CM use only** PR is ready for CI testing on Gaea C6 CI-Gaeac6-Building **Bot use only** CI testing is cloning/building on Gaea C6 labels Oct 6, 2026
@emcbot emcbot added the CI-Gaeac6-Running **Bot use only** CI testing on Gaea C6 for this PR is in-progress label Oct 6, 2026
@emcbot

emcbot commented Oct 6, 2026

Copy link
Copy Markdown

C96_gcafs_cycled FAILED on Gaeac6 (pipeline ID: 113072)

In directory: /gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/EXPDIR/C96_gcafs_cycled_f16ac8d5-113072

Error Log Files:


/gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/COMROOT/C96_gcafs_cycled_f16ac8d5-113072/logs/2021122018/gcdas_fetch.log
/gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/COMROOT/C96_gcafs_cycled_f16ac8d5-113072/logs/2021122100/gcdas_fetch.log

View Error Logs: (gcdas_fetch.log) (gcdas_fetch.log)

This failure was detected automatically by global-workflow's CI/CD Pipeline

@emcbot

emcbot commented Oct 6, 2026

Copy link
Copy Markdown

C96_gcafs_cycled_noDA FAILED on Gaeac6 (pipeline ID: 113072)

In directory: /gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/EXPDIR/C96_gcafs_cycled_noDA_f16ac8d5-113072

Error Log Files:


/gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/COMROOT/C96_gcafs_cycled_noDA_f16ac8d5-113072/logs/2021122018/gcdas_fetch.log
/gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/COMROOT/C96_gcafs_cycled_noDA_f16ac8d5-113072/logs/2021122100/gcdas_fetch.log

View Error Logs: (gcdas_fetch.log) (gcdas_fetch.log)

This failure was detected automatically by global-workflow's CI/CD Pipeline

@emcbot emcbot added CI-Gaeac6-Failed **Bot use only** CI testing on Gaea C6 for this PR has failed and removed CI-Gaeac6-Running **Bot use only** CI testing on Gaea C6 for this PR is in-progress labels Oct 6, 2026
@TravisElless-NOAA

Copy link
Copy Markdown
Contributor

Gaea failures are because HPSS is down and not caused by this PR

@emcbot

emcbot commented Oct 6, 2026

Copy link
Copy Markdown

C96_gcafs_cycled FAILED on Gaeac6 (pipeline ID: 113072)

In directory: /gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/EXPDIR/C96_gcafs_cycled_f16ac8d5-113072

Error Log Files:


/gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/COMROOT/C96_gcafs_cycled_f16ac8d5-113072/logs/2021122018/gcdas_fetch.log
/gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/COMROOT/C96_gcafs_cycled_f16ac8d5-113072/logs/2021122100/gcdas_fetch.log

View Error Logs: (gcdas_fetch.log) (gcdas_fetch.log)

This failure was detected automatically by global-workflow's CI/CD Pipeline

@emcbot

emcbot commented Oct 6, 2026

Copy link
Copy Markdown

C96_gcafs_cycled_noDA FAILED on Gaeac6 (pipeline ID: 113072)

In directory: /gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/EXPDIR/C96_gcafs_cycled_noDA_f16ac8d5-113072

Error Log Files:


/gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/COMROOT/C96_gcafs_cycled_noDA_f16ac8d5-113072/logs/2021122018/gcdas_fetch.log
/gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/COMROOT/C96_gcafs_cycled_noDA_f16ac8d5-113072/logs/2021122100/gcdas_fetch.log

View Error Logs: (gcdas_fetch.log) (gcdas_fetch.log)

This failure was detected automatically by global-workflow's CI/CD Pipeline

@emcbot

emcbot commented Oct 6, 2026

Copy link
Copy Markdown

C48_gsienkf_atmDA FAILED on Gaeac6 (pipeline ID: 113072)

In directory: /gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/EXPDIR/C48_gsienkf_atmDA_f16ac8d5-113072

Error Log Files:


/gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/COMROOT/C48_gsienkf_atmDA_f16ac8d5-113072/logs/2024022400/gdas_prepatmanlbias.log

View Error Logs: (gdas_prepatmanlbias.log)

This failure was detected automatically by global-workflow's CI/CD Pipeline

@emcbot

emcbot commented Oct 6, 2026

Copy link
Copy Markdown

C48_ufsenkf_atmDA FAILED on Gaeac6 (pipeline ID: 113072)

In directory: /gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/EXPDIR/C48_ufsenkf_atmDA_f16ac8d5-113072

Error Log Files:


/gpfs/f6/drsa-precip3/world-shared/global/CI/GITLAB/pr_cases_5294_f16ac8d5_113072/RUNTESTS/COMROOT/C48_ufsenkf_atmDA_f16ac8d5-113072/logs/2024022400/gdas_prepatmanlbias.log

View Error Logs: (gdas_prepatmanlbias.log)

This failure was detected automatically by global-workflow's CI/CD Pipeline

Review feedback: specify the default path in defaults.yaml so that overriding it
is something a user adds to their own experiment generation yaml, matching how
the rest of the configs express a path default.

So drop the default() filter from the four config.base.j2 and add FIXglobal to the
base section of each net's yaml/defaults.yaml.  The ${HOMEglobal} reference passes
through the yaml untouched and is expanded by the shell when the config is sourced,
exactly as the ocn and ice template paths' ${PARMglobal} already is.

This also changes where setup_expt.py records the overlay path.  A config is
rendered with host_plus_inputs_dict updated by yaml_dict[cfg_key], so the yaml
section wins; now that base carries a FIXglobal default, writing the overlay path
to host_plus_inputs_dict would have it silently discarded and the overlay built but
never used.  Write it to yaml_dict['base'] instead, which is also the more honest
place for it: it is a base setting that this experiment overrides.

The test_setup.py end-to-end test covers this by asserting the shell-evaluated
config.base FIXglobal equals the overlay path.
Matches the treatment the template path entries got on the other branch: the
default belongs in defaults.yaml, the explanation belongs in the user guide.
Everything the comment said is already in the "Customizing fix files" section
of docs/source/user_guide/setup.rst.

Also place the key flush in the base key list rather than set off by a blank
line, which was only there to separate it from the comment.
@AlexanderHrabski-NOAA

Copy link
Copy Markdown
Contributor Author

Updated the branch from develop and moved the defaults to defaults.yaml. The 8km workflow runs, so the new feature still works.

TravisElless-NOAA pushed a commit that referenced this pull request Oct 9, 2026
# Description
The pre-rendered input files that provide the MOM6 and CICE6
configuration are maintained as part of the UFS weather model submodule.
If a set of experiments aim to modify the MOM6 and CICE6 configuration,
a user tracks those changes by maintaining a fork of the UFS weather
model. This PR provides makes the `mom_input`, `mom_data_table`, and
`ice_in` template paths YAML-overrideable to allow users to track
changes on these files (a) as part of their experiment's configuration
and (b) without a fork of the weather model.

The override is implemented via Jinja-templating these parameters in
`config.ocn` and `config.ice` to make them overridable from the
experiment setup YAML. In keeping with the established pattern for
Jinja-templated variables with a default value, this PR also moves the
default paths for these files from the runtime parsing utilities to
their respective configuration files.

Note that some additional MOM6 inputs, `mom_override` and `mom_layout`,
are staged as fix files, so they require fix file path customization
(#5294) to override their paths. See #5229 for comments on the
feasibility of additional input file path overrides.

There is no change to default behavior.

@TravisElless-NOAA TravisElless-NOAA left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for everyone's efforts with this PR! With minimal impact to the end user this gives us a chance to explore and expand future directions with the g-w. Therefore approving and merging this feature.

@TravisElless-NOAA
TravisElless-NOAA merged commit 01b3e81 into NOAA-EMC:develop Oct 9, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI-Gaeac6-Failed **Bot use only** CI testing on Gaea C6 for this PR has failed CI-Ursa-Passed **Bot use only** CI testing on Ursa for this PR has completed successfully

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support FIX file customization

5 participants